﻿using System.Windows;
using System.Windows.Input;

namespace FreshMeat.Xaml
{
   /// <summary>
   /// Allows a window to be dragged if the mouse goes down over the element,
   /// which is presumed to be a child of the window.
   /// </summary>
   public class WindowDragBehavior : AttachedBehavior<WindowDragBehavior, Window>
   {
      /// <summary>
      /// Element by which the user can drag the window.
      /// This can be the window itself to allow any visible portion of the window to be used as a drag point.
      /// </summary>
      public static readonly DependencyProperty ElementProperty =
         DependencyProperty.RegisterAttached("Element", typeof(UIElement), typeof(WindowDragBehavior), new PropertyMetadata(null, (d, e) => GetInstance(d)));

      public static UIElement GetElement(Window element)
      {
         return (UIElement)element.GetValue(ElementProperty);
      }

      public static void SetElement(Window element, UIElement value)
      {
         element.SetValue(ElementProperty, value);
      }

      /// <summary>
      /// Indicates whether the behavior is currently performing a drag operation.
      /// This must be set by the same object which set the LayerProperty.
      /// It is intended to be bound to a model using "Mode=TwoWay" so that the
      /// model can be informed as to the current drag state.  The model should
      /// not attempt to change the value of this property (it is only "TwoWay"
      /// because not all XAML flavors support "OneWayToSource", which is the
      /// real intention.
      /// </summary>
      public static readonly DependencyProperty IsDraggingProperty =
         DependencyProperty.RegisterAttached("IsDragging", typeof(bool), typeof(WindowDragBehavior), new PropertyMetadata(false));

      public static bool GetIsDragging(FrameworkElement element)
      {
         return (bool)element.GetValue(IsDraggingProperty);
      }

      public static void SetIsDragging(Window element, bool value)
      {
         // DO NOTHING when this is called -- this property is intended to be "read-only" but that's
         // an odd concept for an attached property used in XAML, so this is a regular attached property
         // which happens not to do anything when written to.
      }

      const double DragTriggerDistance = 2.0;
      const double DragTriggerDistanceSquared = DragTriggerDistance * DragTriggerDistance;

      bool isPossibleStart;
      Point mouseDownPoint;
      bool isDragging;
      Point offset;

      protected override void OnAttached()
      {
         var associatedObject = TryGetAssociatedObject();
         var element = GetElement(associatedObject);
         if (element == null)
            return;

         element.MouseLeftButtonDown += MouseLeftButtonDown;
         element.MouseLeftButtonUp += MouseLeftButtonUp;
         element.MouseMove += MouseMove;
      }

      protected override void OnDetaching()
      {
         var associatedObject = TryGetAssociatedObject();
         if (associatedObject == null)
            return;

         var element = GetElement(associatedObject);
         if (element == null)
            return;

         element.MouseLeftButtonDown -= MouseLeftButtonDown;
         element.MouseLeftButtonUp -= MouseLeftButtonUp;
         element.MouseMove -= MouseMove;
      }

      /// <summary>
      /// Handles the left mouse button going down, which is the first step towards starting a drag.
      /// </summary>
      void MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
      {
         if (e.Handled)
            return;

         // Early-out avoids unnecessary work
         if (isPossibleStart || isDragging)
            return;

         var associatedObject = TryGetAssociatedObject();
         if (associatedObject == null)
         {
            Reset(null);
            return;
         }

         e.Handled = true;

         mouseDownPoint = GetMousePosition(associatedObject, e);
         isPossibleStart = true;
      }

      /// <summary>
      /// Handles the mouse move, which is either the second step towards starting a drag or
      /// is a continuation of a drag operation.
      /// </summary>
      void MouseMove(object sender, MouseEventArgs e)
      {
         // Early-out avoids unnecessary work
         if (!isPossibleStart && !isDragging)
            return;

         var associatedObject = TryGetAssociatedObject();
         if (associatedObject == null)
         {
            Reset(null);
            return;
         }
         var element = GetElement(associatedObject);
         if (element == null)
         {
            Reset(associatedObject);
            return;
         }

         if (isPossibleStart)
         {
            // Mouse must move at least the trigger distance from the original point
            // where it went down in order to start a drag operation.
            var point = GetMousePosition(associatedObject, e);
            var a = point.X - mouseDownPoint.X;
            var b = point.Y - mouseDownPoint.Y;
            if (a * a + b * b > DragTriggerDistanceSquared)
            {
               isPossibleStart = false;

               // If mouse can be captured then start dragging.
               if (element.CaptureMouse())
               {
                  isDragging = true;

                  offset = new Point(mouseDownPoint.X - associatedObject.Left, mouseDownPoint.Y - associatedObject.Top);

                  associatedObject.SetValue(IsDraggingProperty, true);
               }
            }
         }
         // Don't make this an "else" clause -- if the drag was started above then this must be done immediately.
         // This also handles drags that were already in progress.
         if (isDragging)
            MoveTo(associatedObject, e);
      }

      /// <summary>
      /// Handles the left mouse button being released, which either aborts a drag
      /// before it ever gets going or ends an actual drag operation.
      /// </summary>
      void MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
      {
         if (e.Handled)
            return;

         // Early-out avoids unnecessary work
         if (!isPossibleStart && !isDragging)
            return;

         var associatedObject = TryGetAssociatedObject();
         if (associatedObject == null)
         {
            Reset(null);
            return;
         }

         e.Handled = true;

         if (isDragging)
            MoveTo(associatedObject, e);

         Reset(associatedObject);
      }

      static Point GetMousePosition(Window associatedObject, MouseEventArgs e)
      {
         return associatedObject.PointToScreen(e.GetPosition(associatedObject));
      }

      void MoveTo(Window associatedObject, MouseEventArgs e)
      {
         var position = GetMousePosition(associatedObject, e);

         associatedObject.Left = position.X - offset.X;
         associatedObject.Top = position.Y - offset.Y;
      }

      /// <summary>
      /// Resets everything back to a non-dragging state.
      /// Safe to call at any point.
      /// </summary>
      void Reset(Window associatedObject)
      {
         if (associatedObject != null)
         {
            var element = GetElement(associatedObject);
            if (element != null)
               element.ReleaseMouseCapture();

            associatedObject.SetValue(IsDraggingProperty, false);
         }

         isPossibleStart = false;
         isDragging = false;
      }
   }
}
